package watch

import (
	"bytes"
	"context"
	"io/fs"
	"log"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"gitee.com/go-errors/errors"
)

const ignoreFn = "wena.ignore"
const ignore2Fn = ".wena.ignore"

type Watcher struct {
	dir string
	fsw *fsWatcher

	defaultIgnore []byte
	matcher       *Ignore
	onChanged     func(ev Event)
	done          chan struct{}
	isClosed      bool
	mu            sync.Mutex
}

func New(dir string, defaultIgnore []byte) (*Watcher, error) {
	fsw, err := fsNewWatcher()
	if err != nil {
		return nil, errors.Wrap(err)
	}
	w := &Watcher{dir: dir, fsw: fsw, done: make(chan struct{}), defaultIgnore: defaultIgnore}
	w.addDIR()
	return w, nil
}

func (w *Watcher) WithDefaultIgnore(defaultIgnore []byte) *Watcher {
	w.defaultIgnore = defaultIgnore
	return w
}

func (w *Watcher) loadIgnore() {
	data := &bytes.Buffer{}
	data.Write(w.defaultIgnore)
	data.WriteByte('\n')
	if fv, err := os.ReadFile(filepath.Join(w.dir, ignoreFn)); err == nil {
		data.Write(fv)
		data.WriteByte('\n')
	} else {
		log.Println(err)
	}
	if fv, err := os.ReadFile(filepath.Join(w.dir, ignore2Fn)); err == nil {
		data.Write(fv)
		data.WriteByte('\n')
	} else {
		log.Println(err)
	}
	w.matcher = newIgnore(data, w.dir)
}

// addDIR 监控目录
func (w *Watcher) addDIR() {
	w.loadIgnore()
	_ = filepath.WalkDir(w.dir, func(path string, d fs.DirEntry, err error) error {
		if d.IsDir() {
			if !w.isIgnored(path, true) {
				log.Println("watch:", path)
				return w.fsw.Add(path)
			} else {
				return filepath.SkipDir
			}
		}
		return nil
	})
}

func (w *Watcher) Watch(ctx context.Context) error {
	for {
		select {
		case <-ctx.Done():
			return w.Close()
		case <-w.done:
			break
		case ev := <-w.fsw.Events:
			w.onEvent(ev)
		case err := <-w.fsw.Errors:
			return errors.Wrap(err)
		}
	}
}

func (w *Watcher) SetOnChanged(handler func(ev Event)) {
	w.onChanged = handler
}

func (w *Watcher) Close() error {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.isClosed {
		return nil
	}
	w.isClosed = true
	close(w.done)
	return errors.Wrap(w.fsw.Close())
}

func (w *Watcher) onEvent(ev Event) {
	if w.onChanged == nil || ev.Name == "" || !isAnyOp(ev, fsRename, fsRemove, fsCreate, fsWrite) {
		return
	}

	if ev.Name == filepath.Join(w.dir, ignoreFn) || ev.Name == filepath.Join(w.dir, ignore2Fn) {
		log.Println("[watch] => load ignores")
		w.loadIgnore()
		return
	}

	d, _ := isDIR(ev.Name)
	if w.isIgnored(ev.Name, d) {
		return
	}

	switch {
	case isAnyOp(ev, fsRename, fsRemove):
		_ = w.fsw.Remove(ev.Name)
		return
	case isAnyOp(ev, fsCreate):
		if d, _ := isDIR(ev.Name); d {
			_ = w.fsw.Add(ev.Name)
		}
	case isAnyOp(ev, fsWrite):
	default:
		return
	}

	go w.onChanged(ev)
}

func (w *Watcher) isIgnored(name string, isDIR bool) bool {
	ignored := strings.HasSuffix(name, "~") || (w.matcher != nil && w.matcher.Match(name, isDIR))
	log.Println("isIgnored:", name, isDIR, ignored)
	return ignored
}

func isAnyOp(ev Event, ops ...Op) bool {
	for _, op := range ops {
		if ev.Op&op == op {
			return true
		}
	}
	return false
}

func isDIR(fn string) (bool, error) {
	stat, err := os.Stat(fn)
	return stat != nil && stat.IsDir(), err
}

func isFile(fn string) (bool, error) {
	stat, err := os.Stat(fn)
	return stat != nil && !stat.IsDir(), err
}
